import os
import re
import json
import pyparsing as pp

def parse(filename):
    valid_char = pp.identbodychars + ' -+-*/()!?.,;\'"`\\#$%^&@=|~'
    string = pp.Word(valid_char)
    token_key = string.set_results_name('key')
    token_value = ':' + string.set_results_name('value', list_all_matches=True)
    token_expr = pp.Combine('[' + token_key + token_value[...] + ']')
    with open(filename, 'r') as f:
        lines = f.readlines()
    token_pattern = re.compile(r'\[.+?\]')
    token_results = []
    for line in lines:
        if re.search(token_pattern, line) is not None:
            for token in re.findall(token_pattern, line):
                token_results.append(token_expr.parse_string(token))
    return token_results


def construct(results, spec=None, mat_template=None, sub=False):

    def load_spec(spec):
        specname = 'spec/{}_spec.json'.format(spec)
        with open(specname, 'r') as f:
            token_spec = json.load(f)
        return token_spec

    def pick_value(raw):
        if isinstance(raw, pp.ParseResults):
            if len(raw) == 1:  # single value
                return raw[0]
            else:  # value list
                return list(raw)
        else:  # fallback: no type
            return raw

    def pred_type(name, value, spec):
        predefined_types = load_spec(spec)
        # strip value
        if len(value) == 1:
            type_name = value
            value = True
        else:
            type_name = value[0]
            if len(value) == 2:
                value = value[1]
            else:
                value = value[1:]
        if type_name in predefined_types:
            definition = predefined_types[type_name]
            if type(definition) == dict:  # normal type
                name += '/' + type_name
            elif type(definition) == str:  # alias
                type_name = definition  # to initial name
                name += '/' + type_name
            elif type(definition) == list:  #
                names = []
                for item in definition:
                    names.append(name + '/' + item)
                name = names  # to a list
        return name, value

    if sub and (mat_template is not None):
        data = mat_template.copy()
    else:
        data = {}
        template = None
    if spec is not None:
        token_spec = load_spec(spec)
    else:
        token_spec = None
    if sub:
        pointer = data
    else:
        pointer = None
    sub_token = None  # pending sub-structure
    sub_spec = None  # spec for sub-structure
    template = None
    pending = []  # pending lines for sub-structure
    data_type = ''  # for _type key
    for result in results:
        name = result['key']
        if name == 'OBJECT':  # token type definition
            data_type = pick_value(result['value'])
            spec = data_type.lower()
            # dealing with template
            if len(spec) > 9 and spec[-9:] == '_template':
                token_spec = load_spec(spec[:-9])  # remove template suffix
            else:  # normal data
                token_spec = load_spec(spec)
        elif name == spec.upper():  # a new item entry
            # finish possible sub-structure staging
            if sub_token is not None:
                data[item][sub_token] = construct(pending, sub_spec, template, sub=True)
                sub_token, sub_spec, pending = None, None, []
            # start new entry
            item = pick_value(result['value'])
            data[item] = {}  # create a sub object
            pointer = data[item]
            pointer['_type'] = data_type  # first key: item type
        elif name in token_spec:
            definition = token_spec[name]
            value = True  # singleton
            if 'value' in result:
                value = pick_value(result['value'])
            if type(definition) == str:  # sub-structure
                # finish previous sub-structure staging
                if sub_token is not None:
                    data[item][sub_token] = construct(pending, sub_spec, template, sub=True)
                    sub_token, sub_spec, pending = None, None, []
                if type(value) == str:  # value is a string, append to subtoken name
                    sub_token = definition.upper() + '/' + value
                else:
                    sub_token = name
                sub_spec = definition
                # for material sub-tokens, applied predefined templates
                if sub_spec == 'material':  # sub-token
                    mat_name, template = pick_value(result['value'])
                    sub_token = 'MATERIAL/' + mat_name
                    template = mat_template[template].copy()
                elif sub_spec == '/material':  # append to root
                    sub_token = None  # clear sub_token caching
                    sub_spec = sub_spec[1:]
                    template = pick_value(result['value'])
                    template = mat_template[template].copy()
                    for k in template:
                        if k not in pointer:  # do not overwrite exist keys
                            pointer[k] = template[k]
            elif type(definition) == dict:  # normal key
                pointer[name] = value
            elif type(definition) == list:
                if len(definition) == 0:  # zero element: parallel value definition
                    # iterate over names
                    if name not in data:  # not created
                        pointer[name] = []  # new list
                    pointer[name].append(value)
                else:
                    if len(definition) == 1:  # one element: predefined types
                        name, value = pred_type(name, value, definition[0])
                    elif len(definition) > 1:  # multiple elements: shortcut for multiple keys
                        name = definition
                    # for general purpose, always use list here
                    if type(name) == list:
                        for tmp in name:
                            pointer[tmp] = value
                    else:
                        pointer[name] = value
            elif type(definition) == int:  # integer, parallel list with selected key
                if type(value) == list:
                    sub_name = value[definition]
                    value = value[:definition] + value[definition+1:]
                    if len(value) == 1:
                        value = value[0]
                else:
                    sub_name = value
                    value = True
                if name not in pointer:
                    pointer[name] = {}  # new dict
                pointer[name][sub_name] = value
        else:  # not known tokens
            if sub_token is not None:  # logging sub-structure
                pending.append(result)  # cache current token
            else:  # purely unknown token
                print('Error: token [{}] is unknown!'.format(name))
    return data


def generate_docs(database, item_type):
    if not os.path.isdir('out'):
        os.mkdir('out')
    for key in database:
        doc_file = 'out/{:s}_1_{:s}.json'.format(item_type, key)
        with open(doc_file, 'w') as f:
            doc = database[key]
            doc['_name'] = key
            json.dump(database[key], f, indent=2)

